using System;
using System.Collections.Generic;

using System.Text;

using Mud.Client.EventHandlers;
using Mud.Common;
using Mud.Common.Communication;
using Mud.Common.Messages;
using Mud.Common.Physical;

namespace Mud.Client
{
    public class Client
    {

        #region Properties

        #region Private properties

        /// <summary>
        /// Command connection.
        /// </summary>
        private PackageTransporter _commandConnection;

        /// <summary>
        /// Event connection.
        /// </summary>
        private PackageTransporter _eventConnection;

        private string _password;
        private string _ipAddress;
        private int _port;

        private MessageHandlerFactory<IEventHandler> _handlerFactory = new MessageHandlerFactory<IEventHandler>();

        private const int CommandResponseTimeout = 5;

        #endregion

        /// <summary>
        /// Current session id.
        /// </summary>
        public int SessionId
        {
            get { return _sessionId; }
        }
        private int _sessionId;

        /// <summary>
        /// Connected user name.
        /// </summary>
        public string UserName
        {
            get { return _userName; }
        }
        private string _userName;

        /// <summary>
        /// Server name
        /// </summary>
        public string ServerName
        {
            get { return _serverName; }
        }
        private string _serverName;


        /// <summary>
        /// Current player character.
        /// </summary>
        public Creature PlayerCharacter
        {
            get { return _character; }
            internal set {
                if (_character != value)
                {
                    Creature old = _character;
                    _character = value;
                    if (ClientNewPlayerCharacter != null)
                        ClientNewPlayerCharacter(old, _character);
                }
            }
        }
        private Creature _character;


        /// <summary>
        /// Current player area.
        /// </summary>
        public Area PlayerArea
        {
            get { return _area; }
            internal set {
                if (_area != value)
                {
                    Area old = _area;
                    _area = value;
                    if (ClientAreaChanged != null)
                        ClientAreaChanged(old, _area);
                }
            }
        }
        private Area _area;


        /// <summary>
        /// Client shell.
        /// </summary>
        public IClientShell ClientShell
        {
            get { return _shell; }
        }
        private IClientShell _shell;


        /// <summary>
        /// True if we are connected.
        /// </summary>
        public bool Connected
        {
            get { return _connected; }
            private set {
                if (_connected != value)
                {
                    _connected = value;
                    if (_connected && ClientConnected != null)
                        ClientConnected();
                    if (!_connected && ClientDisconnected != null)
                        ClientDisconnected();
                }
            }
        }
        private bool _connected;

        /// <summary>
        /// True if we are playing.
        /// </summary>
        public bool Playing
        {
            get { return _playing; }
            private set {
                if (_playing != value)
                {
                    _playing = value;
                    if (_playing && ClientStartedPlaying != null)
                        ClientStartedPlaying();
                    if (!_playing && ClientEndedPlaying != null)
                        ClientEndedPlaying();
                }
            }
        }
        private bool _playing;



        #endregion

        #region Events

        public delegate void NewPlayerCharacterHandler(Creature oldCharacter, Creature newCharacter);

        public delegate void AreaChangedHandler(Area oldArea, Area newArea);

        public delegate void ClientChangedHandler();

        /// <summary>
        /// Event raised when the player character of the client changes.
        /// </summary>
        public event NewPlayerCharacterHandler ClientNewPlayerCharacter;

        /// <summary>
        /// Event raised when the client connects.
        /// </summary>
        public event ClientChangedHandler ClientConnected;

        /// <summary>
        /// Event raised when the client disconnect.
        /// </summary>
        public event ClientChangedHandler ClientDisconnected;

        /// <summary>
        /// Event raised when the client starts playing.
        /// </summary>
        public event ClientChangedHandler ClientStartedPlaying;

        /// <summary>
        /// Event raised when the client ends playing.
        /// </summary>
        public event ClientChangedHandler ClientEndedPlaying;

        /// <summary>
        /// Event raised when the client area changes.
        /// </summary>
        public event AreaChangedHandler ClientAreaChanged;

        #endregion

        #region Public methods

        /// <summary>
        /// Connects the client to specific server.
        /// </summary>
        /// <param name="ipAddress">Server address.</param>
        /// <param name="port">Server port.</param>
        /// <param name="userName">User name to use.</param>
        /// <param name="password">Password to use.</param>
        /// <returns>True if connected.</returns>
        public bool Connect(string ipAddress, int port, string userName, string password)
        {
            if (Connected)
                return false;
            try
            {


                Logger.Log(LogType.Info, "Client.Connect", " ");
                Logger.Log(LogType.Info, "Client.Connect", " ");
                Logger.Log(LogType.Info, "Client.Connect", "--------------------------------------------------------");
                Logger.Log(LogType.Info, "Client.Connect", "Client starting.");
                Logger.Log(LogType.Info, "Client.Connect", "--------------------------------------------------------");
                
                _eventConnection = new PackageTransporter();
                _commandConnection = new PackageTransporter();
                _userName = userName;
                _password = password;
                _ipAddress = ipAddress;
                _port = port;

                if (!RegisterConnection(_commandConnection, true))
                {
                    Disconnect();
                    return false;
                }
                if (!RegisterConnection(_eventConnection, false))
                {
                    Disconnect();
                    return false;
                }

                RefreshServerInfo();
                Logger.Log(ClientLogTypes.Status, "Client.Connect", "Connected to server {0}:{1} as {2}; Session Id {3}", ipAddress, port, userName, _sessionId);

                Connected = true;
                return true;
            }
            catch (Exception ex)
            {
                Logger.Log(LogType.Error, "Client.Connect", "Unable to connect to server {0}:{1} as {2}. Error {3}.", ipAddress, port, userName, ex);
                return false;
            }
        }


        public bool Logout()
        {
            if (Connected)
            {
                MessageCommandLogout msg = new MessageCommandLogout();
                IMessage response = SendCommand(msg);
                if (response != null && MessageResponseOk.Instance != response)
                {
                    Logger.Log(LogType.Info, "Client.Logout", "Server refused disconnect. {0}", MessageHelper.GetErrorMessage(response));
                    return false;
                }
                Logger.Log(LogType.Info, "Client.Logout", "Server confirmed the logout request.");
                return true; // server will disconnect us
            }
            Logger.Log(LogType.Error, "Client.Logout", "Unable to logout, client is not connected.");
            return false;
        }


        /// <summary>
        /// Disconnects this client from the server forcefully.
        /// </summary>
        public void Disconnect()
        {
            if (_eventConnection != null)
                _eventConnection.Dispose();
            if (_commandConnection != null)
                _commandConnection.Dispose();
            _eventConnection = null;
            _commandConnection = null;
            _sessionId = 0;
            Playing = false;
            Connected = false;
            Logger.Log(ClientLogTypes.Status, "Client.Disconnect", "Disconnected.");
        }

        /// <summary>
        /// Starts to play.
        /// </summary>
        /// <returns>True if we have sucessfuly started playing.</returns>
        public bool StartPlaying()
        {
            Creature character = SelectCharacter();
            if (character == null)
                return false;

            MessageCommandPlay command = new MessageCommandPlay();
            command.CharacterName = character.Name;

            IMessage response = SendCommand(command);
            MessageResponsePlay responsePlay = response as MessageResponsePlay;
            if (responsePlay == null) {
                Logger.Log(LogType.Error, "Client.StartPlaying", "Unable to start playing. {0}", MessageHelper.GetErrorMessage(response));
                return false;
            }

            this.PlayerCharacter = MessageToObjectMapper.MapToObject(responsePlay.PlayerCharacter);

            Playing = true;
            return true;
        }

        public void RefreshServerInfo()
        {
            MessageCommandGetServerInfo msg = new MessageCommandGetServerInfo();
            IMessage response = SendCommand(msg);
            MessageResponseServerInfo info = MessageHelper.AssertIsMessage<MessageResponseServerInfo>(response);
            _serverName = info.ServerName;
        }

        /// <summary>
        /// This is the main client 'pump'. Client shell is responsible for calling this
        /// method periodically.
        /// </summary>
        public void Tick()
        {
            if (!Connected)
                return;
            if (!Playing)
                return;

            CheckEvents();
        }

        #endregion

        #region Client requests

        public void RequestMoveCharacter(Location newLocation, MovementType type)
        {
            if (_character.Location == newLocation)
                return;

            MessageCommandCreatureMove command = new MessageCommandCreatureMove();
            command.X = newLocation.X;
            command.Y = newLocation.Y;
            command.MovementType = type;
            IMessage response = SendCommand(command);
            if (!(response is MessageResponseOk))
            {
                Logger.Log(ClientLogTypes.Status, "Client.RequestMoveCharacter", "You can not move there. {0}", MessageHelper.GetErrorMessage(response));
            }
        }

        #endregion

        public Client(IClientShell shell)
        {
            if (shell == null)
                throw new ArgumentNullException("shell");
            _shell = shell;
        }

        #region Helper methods

        private Creature SelectCharacter()
        {
            MessageCommandGetPlayerCharacters command = new MessageCommandGetPlayerCharacters();
            IMessage response = SendCommand(command);
            MessageResponseGetPlayerCharacters characterResponse
               = response as MessageResponseGetPlayerCharacters;

            if (characterResponse == null)
            {
                Logger.Log(ClientLogTypes.Status, "Client.SelectCharacter", "Unable to get characters. Error: {0}.", MessageHelper.GetErrorMessage(response));
                return null;
            }

            List<Creature> characters = new List<Creature>();
            foreach (MessageStructureCreatureInfo info in characterResponse.Characters)
            {
                Creature creature = MessageToObjectMapper.MapToObject(info);
                characters.Add(creature);
            }
            if (characters.Count == 0)
            {
                Logger.Log(ClientLogTypes.Status, "Client.SelectCharacter", "You have no characters on this server.");
                return null;
            }
            return _shell.SelectCreature(characters);
        }



        private void CheckEvents()
        {
            while (_connected && _eventConnection.Status == TransporterStatus.Connected && _eventConnection.DataAvailable())
            {
                if (_eventConnection.Status != TransporterStatus.Connected)
                {
                    Disconnect();
                    return;
                }


                if (!_eventConnection.Read() && _eventConnection.Status != TransporterStatus.Connected)
                {
                    Disconnect();
                    return;
                }
                while (_eventConnection.IncomingPackageQueue.Count > 0)
                {
                    BytePackage package = _eventConnection.IncomingPackageQueue.Dequeue();
                    IMessage msg = MessageFactory.DeserializeMessage(package);
                    if (msg == null)
                    {
                        Logger.Log(LogType.Error, "Client.CheckEvents", "Received unknown message.");
                        Disconnect();
                        return;
                    }


                    IEventHandler handler = _handlerFactory.CreateHandler(msg);
                    if (handler == null)
                    {
                        Logger.Log(LogType.Error, "Client.CheckEvents", "There is no handler for messsage {0}.", msg);
                        Disconnect();
                        return;
                    }

                    handler.Handle(msg, this);
                    if (_eventConnection == null || _eventConnection.Status != TransporterStatus.Connected)
                    {
                        Disconnect();
                        return;
                    }

                }
            }
        }

        #region Communication methods

        /// <summary>
        /// Registers the connection.
        /// </summary>
        /// <param name="connection">The connection.</param>
        /// <param name="commandConnection">if set to <c>true</c> then this connection is a command connection.</param>
        /// <returns></returns>
        private bool RegisterConnection(PackageTransporter connection, bool commandConnection)
        {
            if (!connection.Connect(_ipAddress, _port))
            {
                Logger.Log(LogType.Error, "Client.RegisterConnection", "Unable to connect to the server {0}:{1}.", _ipAddress, _port);
                return false;
            }
            MessageCommandLogin login = new MessageCommandLogin();
            login.UserName = _userName;
            login.UserPassword = _password;
            login.CommandTransporter = commandConnection;
            if (_sessionId != 0)
                login.SessionId = _sessionId;

            IMessage response = SendCommand(connection, login);
            if (response == null)
                return false;

            if (response.MessageTypeId != (int)MessageTypeIds.ResponseLoginOk)
            {
                Logger.Log(LogType.Error, "Client.RegisterConnection", "Server rejected username '{0}' and password '{1}'. Reason {2}.", _userName, _password, MessageHelper.GetErrorMessage(response));
                return false;
            }
            _sessionId = ((MessageResponseLoginOk)response).SessionId;
            return true;
        }

        /// <summary>
        /// Sends a command through the command connection.
        /// </summary>
        /// <param name="command">Command to send.</param>
        /// <returns>Server response or null if send failed for any reason.</returns>
        private IMessage SendCommand(IMessage command)
        {
            return SendCommand(_commandConnection, command);
        }


        /// <summary>
        /// Sends a command through the connection. If there is any problem with sending commands
        /// then disconnect the client.
        /// </summary>
        /// <param name="connection">Connection to use for sending command.</param>
        /// <param name="command">Command to send.</param>
        /// <returns>Server response or null if send failed for any reason.</returns>
        private IMessage SendCommand(PackageTransporter connection, IMessage command)
        {
            try
            {
                if (connection == null || connection.Status != TransporterStatus.Connected)
                {
                    Logger.Log(LogType.Error, "Client.SendCommand", "Connection not existing or not connected.");
                    Disconnect();
                    return null;
                }
                Logger.Log(LogType.ExtraDebug, "Client.SendCommand", "Sending command {0}.", command);

                connection.OutgoingPackageQueue.Enqueue(MessageUtil.Pack(command));
                if (!connection.Write())
                {
                    Logger.Log(LogType.Error, "Client.SendCommand", "Unable to send command {0}.", command);
                    Disconnect();
                    return null;
                }
                System.Threading.Thread.Sleep(30);

                int lastcount = Environment.TickCount;
                while (connection.IncomingPackageQueue.Count == 0)
                {
                    if (((Environment.TickCount - lastcount) / (1000.0)) > CommandResponseTimeout)
                    {
                        Logger.Log(LogType.Error, "Client.SendCommand", "After waiting for {0} seconds no command response was received.", CommandResponseTimeout);
                        Disconnect();
                        return null;
                   }
                   lastcount = Environment.TickCount;
                    if (!connection.Read() && connection.Status != TransporterStatus.Connected)
                    {
                        Logger.Log(LogType.Error, "Client.SendCommand", "Unable to read response to command {0}.", command);
                        Disconnect();
                        return null;
                    }
                    if (connection.IncomingPackageQueue.Count > 0)
                    {
                        BytePackage package = connection.IncomingPackageQueue.Dequeue();
                        IMessage response = MessageFactory.DeserializeMessage(package);
                        if (response == null)
                        {
                            Logger.Log(LogType.Error, "Client.SendCommand", "Received unknown message.");
                            Disconnect();
                            return null;
                        }

                        Logger.Log(LogType.ExtraDebug, "Client.SendCommand", "Got response {0}.", response);
                        return response;
                    }
                    System.Threading.Thread.Sleep(5);
                }
                throw new InvalidOperationException("This should never happen.");
            }
            catch (Exception ex)
            {
                Logger.Log(LogType.Error, "Client.SendCommand", "There was an error while sending command. Error {0}", ex);
                Disconnect();
                return null;
            }
        }


        #endregion

        #endregion

    }
}
